// ##########################################################################
// #                                                                        #
// #                              CLOUDCOMPARE                              #
// #                                                                        #
// #  This program is free software; you can redistribute it and/or modify  #
// #  it under the terms of the GNU General Public License as published by  #
// #  the Free Software Foundation; version 2 or later of the License.      #
// #                                                                        #
// #  This program is distributed in the hope that it will be useful,       #
// #  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
// #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          #
// #  GNU General Public License for more details.                          #
// #                                                                        #
// #          COPYRIGHT: EDF R&D / TELECOM ParisTech (ENST-TSI)             #
// #                                                                        #
// ##########################################################################

#include "ccCone.h"

// Local
#include "ccNormalVectors.h"
#include "ccPointCloud.h"

ccCone::ccCone(PointCoordinateType bottomRadius,
               PointCoordinateType topRadius,
               PointCoordinateType height,
               PointCoordinateType xOff /*=0*/,
               PointCoordinateType yOff /*=0*/,
               const ccGLMatrix*   transMat /*=nullptr*/,
               QString             name /*="Cylinder"*/,
               unsigned            precision /*=DEFAULT_DRAWING_PRECISION*/,
               unsigned            uniqueID /*=ccUniqueIDGenerator::InvalidUniqueID*/)

    : ccGenericPrimitive(name, transMat, uniqueID)
    , m_bottomRadius(std::abs(bottomRadius))
    , m_topRadius(std::abs(topRadius))
    , m_xOff(xOff)
    , m_yOff(yOff)
    , m_height(std::abs(height))
{
	setDrawingPrecision(std::max<unsigned>(precision, MIN_DRAWING_PRECISION)); // automatically calls buildUp & applyTransformationToVertices
}

ccCone::ccCone(QString name /*="Cylinder"*/)
    : ccGenericPrimitive(name)
    , m_bottomRadius(0)
    , m_topRadius(0)
    , m_xOff(0)
    , m_yOff(0)
    , m_height(0)
{
}

ccGenericPrimitive* ccCone::clone() const
{
	return finishCloneJob(new ccCone(m_bottomRadius, m_topRadius, m_height, m_xOff, m_yOff, &m_transformation, getName(), m_drawPrecision));
}

bool ccCone::buildUp()
{
	if (m_drawPrecision < MIN_DRAWING_PRECISION)
		return false;

	// invalid dimensions?
	if (CCCoreLib::LessThanEpsilon(m_height) || CCCoreLib::LessThanEpsilon(m_bottomRadius + m_topRadius))
	{
		return false;
	}

	// topology
	bool singlePointBottom = CCCoreLib::LessThanEpsilon(m_bottomRadius);
	bool singlePointTop    = CCCoreLib::LessThanEpsilon(m_topRadius);
	assert(!singlePointBottom || !singlePointTop);

	unsigned steps = m_drawPrecision;

	// vertices
	unsigned vertCount = 2;
	if (!singlePointBottom)
		vertCount += steps;
	if (!singlePointTop)
		vertCount += steps;
	// normals
	unsigned faceNormCounts = steps + 2;
	// vertices
	unsigned facesCount = steps;
	if (!singlePointBottom)
		facesCount += steps;
	if (!singlePointTop)
		facesCount += steps;
	if (!singlePointBottom && !singlePointTop)
		facesCount += steps;

	// allocate (& clear) structures
	if (!init(vertCount, false, facesCount, faceNormCounts))
	{
		ccLog::Error("[ccCone::buildUp] Not enough memory");
		return false;
	}

	ccPointCloud* verts = vertices();
	assert(verts);
	assert(m_triNormals);

	// 2 first points: centers of the top & bottom surfaces
	CCVector3 bottomCenter = CCVector3(m_xOff, m_yOff, -m_height) / 2;
	CCVector3 topCenter    = CCVector3(-m_xOff, -m_yOff, m_height) / 2;
	{
		// bottom center
		verts->addPoint(bottomCenter);
		CompressedNormType nIndex = ccNormalVectors::GetNormIndex(CCVector3(0, 0, -1).u);
		m_triNormals->addElement(nIndex);
		// top center
		verts->addPoint(topCenter);
		nIndex = ccNormalVectors::GetNormIndex(CCVector3(0, 0, 1).u);
		m_triNormals->addElement(nIndex);
	}

	// then, angular sweep for top and/or bottom surfaces
	{
		PointCoordinateType angle_rad_step = static_cast<PointCoordinateType>(2.0 * M_PI) / static_cast<PointCoordinateType>(steps);
		// bottom surface
		if (!singlePointBottom)
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				CCVector3 P(bottomCenter.x + cos(angle_rad_step * i) * m_bottomRadius,
				            bottomCenter.y + sin(angle_rad_step * i) * m_bottomRadius,
				            bottomCenter.z);
				verts->addPoint(P);
			}
		}
		// top surface
		if (!singlePointTop)
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				CCVector3 P(topCenter.x + cos(angle_rad_step * i) * m_topRadius,
				            topCenter.y + sin(angle_rad_step * i) * m_topRadius,
				            topCenter.z);
				verts->addPoint(P);
			}
		}
		// side normals
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				// slope
				CCVector3 u(-sin(angle_rad_step * i), cos(angle_rad_step * i), 0);
				CCVector3 v(bottomCenter.x - topCenter.x + u.y * (m_bottomRadius - m_topRadius),
				            bottomCenter.y - topCenter.y - u.x * (m_bottomRadius - m_topRadius),
				            bottomCenter.z - topCenter.z);
				CCVector3 N = v.cross(u);
				N.normalize();

				CompressedNormType nIndex = ccNormalVectors::GetNormIndex(N.u);
				m_triNormals->addElement(nIndex);
			}
		}
	}

	// mesh faces
	{
		assert(m_triVertIndexes);

		unsigned bottomIndex = 2;
		unsigned topIndex    = 2 + (singlePointBottom ? 0 : steps);

		// bottom surface
		if (!singlePointBottom)
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				addTriangle(0, bottomIndex + (i + 1) % steps, bottomIndex + i);
				addTriangleNormalIndexes(0, 0, 0);
			}
		}
		// top surface
		if (!singlePointTop)
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				addTriangle(1, topIndex + i, topIndex + (i + 1) % steps);
				addTriangleNormalIndexes(1, 1, 1);
			}
		}

		if (!singlePointBottom && !singlePointTop)
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				unsigned iNext = (i + 1) % steps;
				addTriangle(bottomIndex + i, bottomIndex + iNext, topIndex + i);
				addTriangleNormalIndexes(2 + i, 2 + iNext, 2 + i);
				addTriangle(topIndex + i, bottomIndex + iNext, topIndex + iNext);
				addTriangleNormalIndexes(2 + i, 2 + iNext, 2 + iNext);
			}
		}
		else if (!singlePointTop)
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				unsigned iNext = (i + 1) % steps;
				addTriangle(topIndex + i, 0, topIndex + iNext);
				addTriangleNormalIndexes(2 + i, 2 + iNext, 2 + iNext); // TODO: middle normal should be halfbetween?!
			}
		}
		else // if (!singlePointBottom)
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				unsigned iNext = (i + 1) % steps;
				addTriangle(bottomIndex + i, bottomIndex + iNext, 1);
				addTriangleNormalIndexes(2 + i, 2 + iNext, 2 + i); // TODO: last normal should be halfbetween?!
			}
		}
	}

	notifyGeometryUpdate();
	showTriNorms(true);

	return true;
}

void ccCone::setHeight(PointCoordinateType height)
{
	if (m_height == height)
		return;

	assert(height > 0);
	m_height = height;

	buildUp();
	applyTransformationToVertices();
}

void ccCone::setBottomRadius(PointCoordinateType radius)
{
	if (m_bottomRadius == radius)
		return;

	assert(radius > 0);
	m_bottomRadius = radius;

	buildUp();
	applyTransformationToVertices();
}

void ccCone::setTopRadius(PointCoordinateType radius)
{
	if (m_topRadius == radius)
		return;

	assert(radius > 0);
	m_topRadius = radius;

	buildUp();
	applyTransformationToVertices();
}

CCVector3 ccCone::getBottomCenter() const
{
	CCVector3  bottomCenter = CCVector3(m_xOff, m_yOff, -m_height) / 2;
	ccGLMatrix trans        = getGLTransformationHistory();
	trans.apply(bottomCenter);
	return bottomCenter;
}
CCVector3 ccCone::getTopCenter() const
{
	CCVector3  topCenter = CCVector3(-m_xOff, -m_yOff, m_height) / 2;
	ccGLMatrix trans     = getGLTransformationHistory();
	trans.apply(topCenter);
	return topCenter;
}

CCVector3 ccCone::getSmallCenter() const
{
	if (m_topRadius <= m_bottomRadius)
	{
		return getTopCenter();
	}
	return getBottomCenter();
}

CCVector3 ccCone::getLargeCenter() const
{
	if (m_topRadius >= m_bottomRadius)
	{
		return getTopCenter();
	}
	return getBottomCenter();
}

PointCoordinateType ccCone::getSmallRadius() const
{
	if (m_topRadius <= m_bottomRadius)
	{
		return m_topRadius;
	}
	return m_bottomRadius;
}

PointCoordinateType ccCone::getLargeRadius() const
{
	if (m_topRadius >= m_bottomRadius)
	{
		return m_topRadius;
	}
	return m_bottomRadius;
}

bool ccCone::toFile_MeOnly(QFile& out, short dataVersion) const
{
	assert(out.isOpen() && (out.openMode() & QIODevice::WriteOnly));
	if (dataVersion < 21)
	{
		assert(false);
		return false;
	}

	if (!ccGenericPrimitive::toFile_MeOnly(out, dataVersion))
	{
		return false;
	}

	// parameters (dataVersion>=21)
	QDataStream outStream(&out);
	outStream << m_bottomRadius;
	outStream << m_topRadius;
	outStream << m_xOff;
	outStream << m_yOff;
	outStream << m_height;

	return true;
}

bool ccCone::fromFile_MeOnly(QFile& in, short dataVersion, int flags, LoadedIDMap& oldToNewIDMap)
{
	if (!ccGenericPrimitive::fromFile_MeOnly(in, dataVersion, flags, oldToNewIDMap))
		return false;

	// parameters (dataVersion>=21)
	QDataStream inStream(&in);
	ccSerializationHelper::CoordsFromDataStream(inStream, flags, &m_bottomRadius);
	ccSerializationHelper::CoordsFromDataStream(inStream, flags, &m_topRadius);
	ccSerializationHelper::CoordsFromDataStream(inStream, flags, &m_xOff);
	ccSerializationHelper::CoordsFromDataStream(inStream, flags, &m_yOff);
	ccSerializationHelper::CoordsFromDataStream(inStream, flags, &m_height);

	return true;
}

short ccCone::minimumFileVersion_MeOnly() const
{
	return std::max(static_cast<short>(21), ccGenericPrimitive::minimumFileVersion_MeOnly());
}

CCVector3 ccCone::computeApex() const
{
	PointCoordinateType smallRadius = getSmallRadius();
	if (CCCoreLib::LessThanEpsilon(smallRadius))
	{
		// cone is 'pointy'
		return getTopCenter();
	}

	CCVector3 smallCenter = getTopCenter();
	CCVector3 largeCenter = getBottomCenter();
	if (smallRadius == m_bottomRadius)
	{
		std::swap(smallCenter, largeCenter);
	}

	PointCoordinateType deltaRadius = getLargeRadius() - smallRadius;
	CCVector3           slope       = (smallCenter - largeCenter) / std::max(deltaRadius, std::numeric_limits<PointCoordinateType>::quiet_NaN());

	return smallCenter + smallRadius * slope;
}

double ccCone::computeHalfAngle_deg() const
{
	double height      = (getTopCenter() - getBottomCenter()).normd();
	double deltaRadius = getLargeRadius() - getSmallRadius();

	return CCCoreLib::RadiansToDegrees(atan2(deltaRadius, height));
}
